Skip to content

[PoC] ASoC: SOF: Move audio support as sof-client (support for multiple cards) - #5815

Draft
ujfalusi wants to merge 100 commits into
thesofproject:topic/sof-devfrom
ujfalusi:peter/topic/multi_card_poc01
Draft

[PoC] ASoC: SOF: Move audio support as sof-client (support for multiple cards)#5815
ujfalusi wants to merge 100 commits into
thesofproject:topic/sof-devfrom
ujfalusi:peter/topic/multi_card_poc01

Conversation

@ujfalusi

Copy link
Copy Markdown
Collaborator

The PR contains my PoC work in progress patches to rewrite SOF and detach the sound from the DSP core driver as 'independent' sof-client.
The DSP management, booting, IPC, PM, ID allocation still remain in the core sdev level, but audio is mostly detached from sdev into snd_sof_instance, which now owns the topology loading, widgets, pipelines, PCMs, component and sound card.

booting with this branch there must not be any functional change, everything should work as before wit the exception that we have a new snd_sof_audio module.
I have introduced two Intel specific module parameters to snd_sof_intel_hda_generic:
multi_card - applicable in case of function topologies and it will create sound cards per main codecs
'split_hdmi` - this will separate only the HDMI card from the main card into it's own.

-- multi_card --

# cat /proc/asound/cards 
 0 [sofcs35l56     ]: sof-cs35l56 - sof-cs35l56
                      DellInc.-DellPro514P514260
 1 [sofhdmi        ]: sof-hdmi - sof-hdmi
                      DellInc.-DellPro514P514260
 2 [sofcs42l45     ]: sof-cs42l45 - sof-cs42l45
                      DellInc.-DellPro514P514260

# cat /proc/asound/pcm 
00-02: Speaker (*) :  : playback 1
00-35: Deepbuffer Speaker (*) :  : playback 1
01-03: HDMI1 (*) : HDMI 1 : playback 1
01-04: HDMI2 (*) : HDMI 2 : playback 1
01-05: HDMI3 (*) : HDMI 3 : playback 1
02-00: Jack Out (*) :  : playback 1
02-01: Jack In (*) :  : capture 1
02-04: Microphone (*) :  : capture 1
02-31: Deepbuffer Jack Out (*) :  : playback 1

# amixer -c0 info
Card sysdefault:0 'sofcs35l56'/'DellInc.-DellPro514P514260'
  Mixer name    : ''
  Components    : ' cfg-amp:2 iec61937-pcm:7,6,5 spk:cs35l56'
  Controls      : 32
  Simple ctrls  : 27

# amixer -c1 info
Card sysdefault:1 'sofhdmi'/'DellInc.-DellPro514P514260'
  Mixer name    : 'Intel Panther Lake HDMI'
  Components    : 'HDA:80862822,80860101,00100000 iec61937-pcm:5,4,3'
  Controls      : 21
  Simple ctrls  : 3

# amixer -c2 info
Card sysdefault:2 'sofcs42l45'/'DellInc.-DellPro514P514260'
  Mixer name    : ''
  Components    : ' cfg-amp:0 iec61937-pcm:7,6,5 hs:cs42l45 mic:cs42l45-dmic'
  Controls      : 36
  Simple ctrls  : 24

# ls -al /sys/bus/auxiliary/drivers/snd_sof_audio/
total 0
drwxr-xr-x.  2 root root    0 Jun 12 16:12 .
drwxr-xr-x. 10 root root    0 Jun 12 16:06 ..
--w-------.  1 root root 4096 Jun 12 16:12 bind
lrwxrwxrwx.  1 root root    0 Jun 12 16:12 module -> ../../../../module/snd_sof_audio
lrwxrwxrwx.  1 root root    0 Jun 12 16:12 snd_sof.audio.0 -> ../../../../devices/pci0000:00/0000:00:1f.3/snd_sof.audio.0
lrwxrwxrwx.  1 root root    0 Jun 12 16:12 snd_sof.audio.1 -> ../../../../devices/pci0000:00/0000:00:1f.3/snd_sof.audio.1
lrwxrwxrwx.  1 root root    0 Jun 12 16:12 snd_sof.audio.2 -> ../../../../devices/pci0000:00/0000:00:1f.3/snd_sof.audio.2
--w-------.  1 root root 4096 Jun 12 16:11 uevent
--w-------.  1 root root 4096 Jun 12 16:12 unbind

-- split_hdmi --

# cat /proc/asound/cards 
 0 [sofsoundwire   ]: sof-soundwire - sof-soundwire
                      DellInc.-DellPro514P514260
 1 [sofhdmi        ]: sof-hdmi - sof-hdmi
                      DellInc.-DellPro514P514260

# cat /proc/asound/pcm 
00-00: Jack Out (*) :  : playback 1
00-01: Jack In (*) :  : capture 1
00-02: Speaker (*) :  : playback 1
00-04: Microphone (*) :  : capture 1
00-31: Deepbuffer Jack Out (*) :  : playback 1
00-35: Deepbuffer Speaker (*) :  : playback 1
01-03: HDMI1 (*) : HDMI 1 : playback 1
01-04: HDMI2 (*) : HDMI 2 : playback 1
01-05: HDMI3 (*) : HDMI 3 : playback 1

# amixer -c0 info
Card sysdefault:0 'sofsoundwire'/'DellInc.-DellPro514P514260'
  Mixer name    : ''
  Components    : ' cfg-amp:2 iec61937-pcm:7,6,5 hs:cs42l45 mic:cs42l45-dmic spk:cs35l56'
  Controls      : 68
  Simple ctrls  : 51

# amixer -c1 info
Card sysdefault:1 'sofhdmi'/'DellInc.-DellPro514P514260'
  Mixer name    : 'Intel Panther Lake HDMI'
  Components    : 'HDA:80862822,80860101,00100000 iec61937-pcm:5,4,3'
  Controls      : 21
  Simple ctrls  : 3

In either case, you can unbind any card runtime w/o affecting the operation of the other cards, like in multi_card, one can remove the sof-hdmi card by:

echo -n "snd_sof.audio.2" > /sys/bus/auxiliary/drivers/snd_sof_audio/unbind 

The sof-hdmi card will always use the skl_hda machine driver with sof-hda-generic-idisp.tplg, so it's PCM list will be constant.

The split_hdmi works with IPC3 hda and sndw machines as well.

The topology name is moved from fw_profile of debugfs to audio.X/topology_name.

The branch should not case regression to @thesofproject/amd, @thesofproject/nxp or @thesofproject/mediatek, it works with nocodec and I2S machine drivers as well.

There are still layering violation around sdev accesses, some code still relies on sdev while it should not, NHLT blob is iffy a bit, but not much worst than what we have, but not ideal.

Concurrent card usage works fine as well, UIs will be broken for the multi_card but likely going to work with split_hdmi, only that we will miss the HDMI from profile (likely) as we keep the original card's name for the card from where the HDMI is removed.

ujfalusi added 30 commits August 7, 2026 11:12
Protect IPC RX and FW state handler list unregister/dispatch paths with
client_event_handler_mutex to match list registration and locking
comments.

Fixes: 5c19da3 ("ASoC: SOF: Use guard()/scoped_guard() for mutex locks where it makes sense")
Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
We need to adjust the params based on the available and picked SSP blob
in the similar way we do for DMIC.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
…election

The dai copier configuration for playback and capture needs to be separated
because it is not correct to configure the dai copier as part of the input
format selection for both direction.
The input format is the dai format for capture, but it is not for playback,
for playback the dai format is on the output side.

Currently we configure and adjust the params based on the DAI supported
formats when configuring the input side of the copier but right after the
format has been adjusted we reset it for playback and loose this
information.

When using a nocodec passthrough topology (which is a bug) we have  SSP
blobs supporting 32bit only, copier supporting 16/24/32 bit then on
playback the dai and copier will be incorrectly configured:
host.copier.in: S16_LE
host.copier.out: S16_LE
dai.copier.in: S16_LE
SSP.blob: S32_LE (we only have S32_LE blobs)
dai.copier.out: S16_LE (the dai constraint is ignored)

To handle such case the handling of capture and playback streams must be
changed:
The input format (no changes to previous implementation):
for playback it is the pipeline_params
for capture it is the adjusted fe_params

The output format (no change for capture direction):
for playback it is the adjusted fe_params
for capture it is the fe_params

with this change path format configuration will be correct:
host.copier.in: S16_LE
host.copier.out: S16_LE
dai.copier.in: S16_LE
SSP.blob: S32_LE (we only have S32_LE blobs)
dai.copier.out: S32_LE

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Print the same information as we print in _sof_ipc4_prepare_copier_module()
since the prepare will be not called on system resume, only the
host_config will be executed and tracking the stream tag for the host is
valuable information.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
…load

sof_ipc3_bytes_ext_put() copies header.length bytes from user space into
cdata->data, but the amount of payload later sent to the firmware is taken
from the ABI header's own size field. Nothing checks that the two agree, so
a user claiming a size larger than the data it actually provided makes the
driver send the stale tail of the previous control value to the DSP. The
same stale tail is returned to user space by a subsequent bytes_ext_get()
that does not read back from the DSP.

Reject the payload if the ABI size field exceeds the data available in the
TLV block. header.length has already been verified to be at least
sizeof(struct sof_abi_hdr), so the subtraction cannot underflow.

Fixes: 67ec2a0 ("ASoC: SOF: Add bytes_ext control IPC ops for IPC3")
Cc: stable@vger.kernel.org
Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
For aggregate DAIs (num_cpus > 1) the pre_trigger/post_trigger callbacks
send pipeline state IPCs per-DAI without considering that multiple DAIs
may share the same pipeline. This causes premature state transitions
where the pipeline goes RUNNING before all link DMAs have started, or
individual DAIs send redundant IPCs for shared pipelines.

Fix this by checking the HDA stream running state in post_trigger:
- START/PAUSE_RELEASE: defer RUNNING IPC until all DAIs sharing the
  same pipeline have their link DMA streams running
- STOP/SUSPEND/PAUSE_PUSH: use pipeline state dedup so the PAUSED IPC
  is sent once regardless of how many DAIs share the pipeline

The running-state check naturally handles all aggregate topologies:
shared pipelines, independent pipelines, and mixed cases without
requiring per-pipeline counters.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
DSP-initiated IPC notifications and firmware state changes are delivered
to clients through two dedicated subscription lists (ipc_rx_handler_list
and fw_state_handler_list), each with its own entry type, allocation and
register/unregister API, all guarded by client_event_handler_mutex.

This does not scale. Adding a new event type requires yet another list,
entry struct and register/unregister pair, duplicating the same
bookkeeping.

Collapse the per-event lists into a single struct sof_client_ops that a
client registers with sof_client_register_ops() from its probe and drops
with sof_client_unregister_ops() from its remove. Adding a new event type
then only adds a callback field to the ops structure.

Rename client_event_handler_mutex to client_ops_mutex and track the
clients that registered ops on client_ops_list. The dispatchers walk that
list and invoke the relevant callback when present, so only clients that
opted in are visited. Clients receive every notification and filter the
message types they handle themselves.

Assisted-by: GitHub-Copilot:claude-opus-4.8
Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
In order for clients to be able to handle notification they are interested,
the sof_client_ipc_rx_dispatcher() must be called from rx handler.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Rename snd_sof_dsp_only_d0i3_compatible_stream_active() to the shorter
snd_sof_dsp_state_is_d0i3_compatible(), which reads more naturally at the
call site. No functional change.

Assisted-by: GitHub-Copilot:claude-opus-4.8
Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The DSP can only enter the low-power D0i3 substate when every active
stream tolerates it. snd_sof_dsp_state_is_d0i3_compatible() decides this
today by scanning the audio PCM streams directly, which does not work once
audio moves into a removable sof-client, and cannot account for a non-audio
client that has its own D0i3-relevant activity.

Add a d0i3_vote callback to struct sof_client_ops. A client reports one of
three states via enum sof_d0i3_vote: no activity, active and D0i3
compatible, or active and incompatible. sof_client_get_d0i3_vote() folds
the votes of all clients that implement the callback:

  - any SOF_D0I3_INCOMPATIBLE           -> SOF_D0I3_INCOMPATIBLE (veto)
  - else any SOF_D0I3_COMPATIBLE_ACTIVE -> SOF_D0I3_COMPATIBLE_ACTIVE
  - else                                -> SOF_D0I3_NO_ACTIVITY

Clients without the callback do not participate. The fold is associative,
so combining the core scan with the client votes yields the same result as
a single scan over all streams.

snd_sof_dsp_state_is_d0i3_compatible() now folds the client vote into its
own scan. No client implements the callback yet, so the vote is
SOF_D0I3_NO_ACTIVITY and the behaviour is unchanged.

Assisted-by: GitHub-Copilot:claude-opus-4.8
Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The mailbox descriptor is a DSP-architecture concept rather than a
private core detail, and it will be needed by sof-client code that
must not include sof-priv.h. Move struct snd_sof_mailbox from
sof-priv.h to include/sound/sof.h, alongside the other shared
DSP-level definitions.

Add enum snd_sof_mailbox_type to identify the individual DSP mailboxes
(FW info, DSP, host, stream, debug), terminated with SOF_MAILBOX_COUNT
for future use.

No functional change.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Add a client accessor that returns the struct snd_sof_mailbox for a
given DSP mailbox, selected by enum snd_sof_mailbox_type. This lets
sof-client code obtain a mailbox descriptor (e.g. the FW info box) and
its offset without reaching into struct snd_sof_dev directly.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Add sof_client_is_dspless() and sof_client_get_num_cores() accessor
functions to the sof-client API. These will be needed by the audio
client driver to query DSP state without direct access to snd_sof_dev.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Add a client accessor returning the IPC4 firmware data (struct
sof_ipc4_fw_data) stored in snd_sof_dev::private. This lets sof-client
IPC4 code obtain the firmware data without reaching into struct
snd_sof_dev directly.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The kcontrols of a widget are parsed before the widget itself, so the
control load handlers assigned scontrol->comp_id by pre-reading the
global next_comp_id counter, relying on the fact that the very next
widget allocation would consume that same id. This coupling is fragile
and reaches into the DSP-global id counter from the control path.

Instead, assign each kcontrol the comp_id of the widget it belongs to
once the widget has been created, walking the widget's kcontrols the
same way the unload path does. The resulting comp_id is identical, but
it is now derived from the owning widget rather than guessed from the
allocator.

No functional change.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
next_comp_id is a DSP-global monotonic comp_id allocator that may be
accessed concurrently once a single DSP backs multiple audio
cards/clients loading topology in parallel. Convert it to atomic_t and
allocate ids with atomic_fetch_inc() so the increment is race-free.

Also drop the misleading "reset during S3" note: the counter is never
reset in code (snd_sof_dev persists across S3), it is only
re-initialised when the device is re-probed.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Add a client accessor that allocates a new unique component id from the
DSP-global monotonic comp_id counter using atomic_fetch_inc(). Component
ids belong to the DSP instance, not to an individual audio client, so a
single allocator shared by all clients keeps the ids unique even when
one DSP backs multiple audio cards. There is no matching free: ids are
only handed out, never released.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The system suspend target is recorded on the SOF device when a system
suspend starts, and the PCM trigger path needs it to decide whether a
D0i3 compatible stream can be left running over S0iX.

Add sof_client_is_suspend_target_s0ix() so that audio code can query
this without dereferencing struct snd_sof_dev. A predicate is used
rather than exposing the suspend target itself, since audio code is
only interested in the S0iX case and enum sof_system_suspend_state is
private to the SOF core.

No functional change, the accessor has no users yet.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The topology file name resolved by the SOF core is needed by audio code,
which should not dereference struct snd_sof_dev to reach it.

Add sof_client_get_topology_name() so that the name can be queried via
the sof-client API.

Note that a client owning its own machine description has its own
topology name and should only fall back to this one when the machine
does not provide it.

No functional change, the accessor has no users yet.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The topology path prefix is used to build the full path of the topology
file to be requested. Audio code needs it both for the topology name of
the SOF device and for the one coming from the client's own machine
description.

Add sof_client_get_topology_prefix() so that it can be queried via the
sof-client API instead of dereferencing struct snd_sof_dev.

The path is a property of the SOF device and it is shared by all
clients, there is no need for a per client copy of it.

No functional change, the accessor has no users yet.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Function topology loading is skipped when the user requested a specific
topology file via the sof_tplg_filename module parameter, as that file
is expected to be loaded as it is.

Add sof_client_is_function_topology_disabled() so that audio code can
query this without dereferencing struct snd_sof_dev.

Note that this is separate from the topology.c local module parameter of
the same name, which stays where it is.

No functional change, the accessor has no users yet.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The machine description selected by the SOF core is needed by audio code
to reach the get_function_tplg_files() callback, but it should not be
obtained by dereferencing struct snd_sof_dev.

Add sof_client_get_machine() to query it via the sof-client API.

The returned machine is shared by all clients. A client which owns a
machine description of its own must use that one and only fall back to
this one when it has none, so the expectation is that clients resolve
the two once and use the result afterwards.

No functional change, the accessor has no users yet.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The sdev level mclk_id_override/mclk_id_quirk pair is set either from
NHLT data or from the snd-sof-intel-hda-generic mclk_id module parameter
and it has to be consulted when an SSP link is configured.

Add sof_client_get_ssp_mclk_id_quirk() so that the quirk can be queried
via the sof-client API instead of dereferencing struct snd_sof_dev.

The accessor combines the two fields into a single query: it returns
true and fills the output parameter only when a quirk is present, so
callers cannot use the value without checking its validity. A signed
sentinel would not work here as struct snd_sof_dev is zero initialized
and 0 is a valid mclk_id.

No functional change, the accessor has no users yet.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Replace direct snd_soc_component_get_drvdata() lookups used to fetch
struct snd_sof_dev with snd_sof_component_get_sdev() across
sound/soc/sof/.

This is a preparatory, mechanical cleanup to keep SOF component
accessor naming consistent ahead of follow-up ownership refactoring.
No functional behavior change is intended.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Convert the PCM callback family in snd_sof_dsp_ops to pass
struct snd_soc_component * instead of struct snd_sof_dev *.

Update the PCM callback wrappers and callback implementations to
retrieve sdev with snd_sof_component_get_sdev(component).

This is a preparatory, mechanical API-churn change only. No
functional behavior change is intended, and sdev->component
assignment is intentionally kept intact for now.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Convert the compress callback family in snd_sof_dsp_ops to pass
struct snd_soc_component * instead of struct snd_sof_dev *.

Update the compress callback wrappers and callback implementations to
retrieve sdev with snd_sof_component_get_sdev(component).

This is a preparatory, mechanical API-churn change only. No
functional behavior change is intended.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Many log messages in the topology, control, PCM and audio-core paths
were printing via sdev->dev even though the context is clearly bound
to a specific component (scomp), widget (swidget), route (sroute) or
control (scontrol).  Logging through sdev->dev in those paths forces
all messages to appear under the platform device, making it impossible
to distinguish which audio component or pipeline instance triggered a
given message.

Switch all such messages to use the device that belongs to the
available context. Several internal helper functions that received
struct snd_sof_dev * solely to reach sdev->dev have also been
simplified to derive sdev locally or drop the parameter entirely.

sdev->dev is retained where there is no component context yet (e.g.
early IPC notification handlers before the widget lookup, platform-
level firmware loading, and places where the allocation is tied to the
lifetime of sdev rather than the component).

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Change sof_ipc4_set_pipeline_state() to take the component instead of
snd_sof_dev and update the Intel HDA call sites accordingly.

This keeps the pipeline state path component-scoped and allows the use
of the component device for logging. The snd_sof_dev is still derived
locally for the IPC transmission.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Drop the snd_sof_dev::component field and the leftover assignment in
sof_pcm_probe().

All users were converted to explicit component arguments or helper-based
lookups in previous preparatory changes, so this pointer is no longer
needed.

No functional behavior change is intended.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
ujfalusi added 19 commits August 7, 2026 11:12
compr_get_dai_frame_counter(), get_dai_frame_counter() and
get_host_byte_counter() are the last stream callbacks still living in
snd_sof_dsp_ops. They are only ever reached from the PCM and compress
delay reporting paths, so the audio code has to keep dereferencing
sdev->pdata->desc->ops to call them, which is exactly what the audio
client must not do.

Move the three callbacks to struct sof_audio_ops and give them the
component based prototype used by the rest of that structure. The
accessors in ops.h resolve the audio instance from the component like
their neighbours, and all call sites already have the component at hand.
sof_ipc4_pcm_setup() takes it from spcm->scomp.

None of the HDA implementations used the snd_sof_dev argument, so drop
it from hda_dsp_get_stream_llp(), hda_dsp_compr_get_stream_llp(),
hda_dsp_get_stream_ldp() and from the hda_dsp_get_llp() helper they
share.

With this no audio path reaches into snd_sof_dsp_ops any more.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
sof_client_audio_suspend() and sof_client_audio_resume() implement the
power management of a single audio instance, but they live in
sof-client.c, which is the generic client infrastructure of the SOF
core. Their only caller is the PM callback pair of the audio client.

Fold them into sof_audio_client_suspend() and sof_audio_client_resume()
and let the audio core provide sof_audio_instance_suspend() and
sof_audio_instance_resume() for the pipeline handling, so that the
instance internals are not accessed from outside of the audio code.

The moved code inspected sdev->dsp_power_state and sdev->fw_state
directly. Use sof_client_get_fw_state() for the latter and add
sof_client_is_dsp_in_d0() for the former to keep the client on the
client API instead of the core internals.

No functional change intended.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The pre-built snd_soc_component_driver of an audio instance is assembled
by sof_audio_client_init_pdata(), which runs in the SOF core when the
audio auxiliary device is registered. The template only describes the
PCM operations of the client that is about to be created, so the client
itself is a better place to build it.

Move the snd_sof_new_platform_drv() call to sof_audio_client_probe()
and convert the function to take the sof_client_dev instead of the
snd_sof_dev, as the clients have no access to the latter. The IPC type
and the DSPless mode already have client accessors, add
sof_client_get_machine_drv_name() for the machine driver name, which
hides whether the machine is described by an ACPI or an OF descriptor.

No functional change intended.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
sof_client_ipc4_find_swidget_by_id() lets the probes client resolve a
module instance to its topology widget. It is implemented in the SOF
core, which walks the audio instances and calls into the audio code to
do the lookup, one of the last places where the core depends on audio.

Turn it into a get_module_name() callback of struct sof_client_ops. The
core only asks the clients which implement the callback, so the lookup
no longer needs the audio symbols. Letting the probes client call into
the audio client instead would tie the two modules together and would
need the audio module to be pinned for as long as probes is loaded.

The callback copies the widget name instead of returning a pointer to
it. Before audio became a client, snd_sof_device_remove() unregistered
all clients before unbinding the machine driver, so the topology could
not be unloaded while the probes client was still around to dereference
a widget. Audio clients are now bound and unbound on their own, so that
ordering no longer holds and the name is copied while the core still
holds the client ops mutex.

The name is only used for a debugfs string, truncation is harmless. It
is not bound by the topology name length as the ASoC core may prefix
the widget name with the component name.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The client event callbacks are dispatched with client_ops_mutex held,
which restricts them to non sleeping work. A callback which sends an
IPC message and waits for the reply would deadlock: the IPC IRQ thread
handles both the replies and the DSP initiated notifications and it
takes the same mutex in sof_client_ipc_rx_dispatcher(). If a
notification arrives while the mutex is held then the IRQ thread blocks
on it and the reply the callback is waiting for can never be delivered.
On Intel platforms the BUSY interrupt is left masked until the
notification handler returns, so the DSP cannot raise the next message
either.

None of the current callbacks sends IPC, but the firmware boot needs to
notify the audio clients to let them restore their pipelines, which is
IPC heavy.

Protect the list with SRCU instead. The mutex is kept to serialize the
writers while the dispatchers use the read side critical section, which
is allowed to sleep. sof_client_unregister_ops() waits for the grace
period to pass before it drops the ops pointer, a client module cannot
be removed while one of its callbacks is still running.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
sof_restore_pipelines() walks the audio instances of the SOF device from
the firmware boot path, which is one of the last places where the core
reaches into the audio stack.

Turn it into a fw_booted client callback: the core tells every client
that the firmware has been (re)booted and that nothing of the state the
client had in the DSP survived it, the audio client sets up the static
pipelines of its own instance. Booting the DSP is aborted if a client
fails to rebuild its state, as it was before.

The set of clients which have a component is the set of the registered
audio instances, sof_pcm_probe() sets the component when it registers
the instance and clears it when the registration fails or the instance
is unregistered, so the same instances are restored as before.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
snd_sof_stream_suspend_ignored() walks the PCMs of every audio instance
of the SOF device to decide whether the DSP can be powered down when the
system suspends to S0IX. The core has no business knowing what a stream
is, and the audio client is not the only one which can leave something
running in the DSP on purpose.

Replace it with a keep_dsp_in_d0 client callback and fold the answers in
sof_client_keep_dsp_in_d0(): any client which needs what it has in the
DSP to survive keeps it in D0. The audio client answers for the streams
of its own instance which ignored the suspend trigger.

The wording is kept generic on purpose, the criteria listed in
snd_sof_dsp_power_target() which are still to be implemented are not
specific to audio streams either.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
snd_sof_sdev_get_tplg_ops() and snd_sof_sdev_get_pcm_ops() return the
ops of the first audio instance found on the SOF device, which is not a
meaningful answer once a device can have several instances.

They have no users left in the tree, remove them.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Every platform pcm_pointer implementation starts with the same
snd_sof_find_spcm_dai() lookup to get the snd_sof_pcm the substream
belongs to.

That lookup is the only reason why the AMD and the MediaTek platform
drivers reference symbols owned by the audio code, which stands in
the way of moving the audio support to a separate module.

The core resolves the very same snd_sof_pcm a few lines after the
callback is invoked, so pass it to the callback instead and drop the
open coded lookup from the platform drivers.  In sof_pcm_pointer()
the lookup is moved ahead of the callback, which also makes the
(theoretical) case of a missing snd_sof_pcm consistently reported as
-EINVAL.

The snd_sof_pcm_platform_pointer() wrapper in ops.h has no users, it
is dropped rather than updated.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
…en()

hda_dsp_compr_open() looks up the snd_sof_pcm for the compressed
stream but it only uses it to bail out if it is not found.  The core
has already done the very same lookup before it calls the platform
callback, so the check can never trigger.

Drop the lookup, it serves no purpose.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The HDaudio pcm_open implementation repeats the snd_sof_find_spcm_dai()
lookup which the core has already done just before it invokes the
callback.

Pass the resolved snd_sof_pcm to the callback and drop the open coded
lookup, removing the last PCM lookup from the platform PCM callbacks.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
sof_ipc4_set_pipeline_state() sends the generic SET_PIPELINE_STATE
message to the firmware.  It is not tied to PCM handling in any way,
it only happens to live in ipc4-pcm.c, which makes it one of the few
symbols the HDaudio DAI code needs from the audio side.

Move it next to sof_ipc4_pipeline_state_str() in ipc4.c and take the
sof_client_dev directly instead of deriving it from the ASoC
component, which is not needed for a plain IPC message.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
snd_sof_dsp_state_is_d0i3_compatible() walked the PCMs of every audio
instance from the core and only then folded in the votes of the other
clients, which made the core depend on the audio code.

Move the PCM walk to the audio client as its d0i3_vote callback, the
callback the core already has for exactly this purpose, and leave
snd_sof_dsp_state_is_d0i3_compatible() as the fold of the client
votes.

The IPC4 side will need to participate in the same vote soon, this
gives it a place to plug into.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The platform interrupt handler has no way to tell which audio instance
owns the DMA stream which just completed a period, it only has the
substream.

Add a period_elapsed client callback and let the clients themselves
decide whether the substream is theirs. The first client claiming the
substream ends the search, the same way as the get_module_name callback
works.

The callback is special among the client ops: it is invoked from the
interrupt handler, which may hold its own locks, so it must not sleep.
The audio client only takes the ownership if the substream is bound to
its own ASoC component, matching on the dai_link id alone is not enough
as the ids are only unique within a card.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The audio instance of an ASoC component was looked up by walking the
audio instance list of the SOF device, which the platform drivers had to
follow as well to get to the topology ops.

There is exactly one audio instance per audio client and the component
already carries the client device as its drvdata, so save the instance in
the client platform data and turn the component level getters into
pointer dereferences.

The getters are inline now, which removes the last symbol reference from
the platform drivers to the audio code. The instance list itself is kept
for the cross instance aggregations.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The last user of the instance list was the HDA D0i3 streaming check,
which walked the PCMs of every audio instance to tell if all the active
streams are D0i3 compatible and at least one of them is playback.

The D0i3 vote of the clients already answers the first half, extend it
with a playback flavour of the compatible vote to answer the second one
as well and fold the votes by their precedence.

With that the instance list, its lock and the iteration macro have no
users left, an audio instance is only reachable via its own component.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The 12 objects implementing the ASoC component (PCM, topology, control
and their IPC3/IPC4 backends) no longer reference anything provided by
the platform drivers, so they can finally leave snd-sof.ko and join the
audio client in snd-sof-audio.ko.

Export the handful of core facilities they still need: the widget,
position and topology tracepoints, sof_ipc4_find_module_by_uuid(),
sof_ipc4_update_cpc_from_manifest() and sof_ipc4_pipeline_state_str().

snd-sof-audio.ko now depends on snd-sof.ko and the platform drivers only
depend on snd-sof.ko, so there is no dependency cycle.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
The tracepoints of the SOF core are declared in a single trace header,
include/trace/events/sof.h. CREATE_TRACE_POINTS can only be defined in
one translation unit per trace header, and that unit defines every event
of the header, so the header has exactly one owning module: core.c in
snd-sof.ko.

Four of the six events are only used by the audio client module, which
made it necessary to export them with EXPORT_TRACEPOINT_SYMBOL_GPL() to
be able to use them from there.

Move sof_widget_setup, sof_widget_free, sof_ipc3_period_elapsed_position
and sof_pcm_pointer_position to a new header,
include/trace/events/sof_audio.h, and instantiate it in the audio
client. Each event is now defined by the module which uses it and none
of them needs to be exported.

The new header keeps TRACE_SYSTEM as sof and only overrides
TRACE_INCLUDE_FILE, so the events stay under the same
/sys/kernel/tracing/events/sof/ directory as before.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
With the audio objects living in snd-sof-audio.ko these symbols are only
used within that module, no need to place them into the kernel's symbol
table.

The remaining exports have users outside of the module: the machine
drivers call the DAI parameter helpers and snd-sof-nocodec.ko uses
sof_pcm_dai_link_fixup().

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
@ujfalusi
ujfalusi force-pushed the peter/topic/multi_card_poc01 branch from 4d87433 to 22fc861 Compare August 7, 2026 08:12
When soc_check_tplg_fes() overrides BE DAI link CPU component names,
only set cpu->name when the DAI is actually registered by the matching
component. This prevents cross-component DAI binding issues when
multiple components provide identically-named DAIs (e.g., SDW DAIs
in soundwire_intel vs SOF audio components).

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
When a function topology does not support a DAI link, continue to the
next one instead of failing the entire topology load. This allows
partial topology loading where not all DAI links have matching function
topologies.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
…links

Before loading a user-specified feature topology, verify that the
card has a matching BE DAI link. Feature topologies extend specific
codec function types (amp, jack, mic) and reference widgets that
only exist when the corresponding BE DAI links are present.

Without this check, loading an amplifier feature topology on a card
without SmartAmp DAI links would fail with -EINVAL due to missing
route endpoints, killing the entire card probe.

This is needed for multi-card configurations where cards are split
by function type, but also provides a safety net in single-card
mode when feature topologies don't match the hardware.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
When more than one card is created on top of the same audio DSP the
sof_sdw machine driver must be able to name its card and to know which
DAI types it owns, both coming from the machine parameters.

Use mach_params.card_name in mc_probe() to name the card when it is
provided and keep the "soundwire" name otherwise.

Add the dai_type_mask field to snd_soc_acpi_mach_params, it is used by
the SoundWire endpoint parsing in a follow-up patch.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Add dai_type_mask field filtering in asoc_sdw_count_sdw_endpoints() and
asoc_sdw_parse_sdw_endpoints() to allow multi-card configurations to
include only specific DAI types.

When dai_type_mask is non-zero, only codec endpoints with matching
dai_type bits are counted and parsed. Auxiliary devices for codecs
without matching endpoints are also skipped.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Add Intel-specific multi-card audio client registration that creates
separate sound cards per codec function type when the multi_card module
parameter is set.

The registration groups SoundWire endpoints by DAI type (jack, amp, mic)
and creates a card per group using codec-specific names (e.g. cs42l43,
cs35l56) with a generic function name fallback (jack, speaker, mic).
DMIC and HDMI are registered as additional separate cards.

Individual card registration failures are non-fatal to allow partial
audio functionality when some components fail to probe.

When multi_card is not set (default), falls back to the generic
single-card sof_register_audio_client().

This is experimental and opt-in via:
  modprobe snd-sof-intel-hda-generic multi_card=1

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Use mach_params.card_name when provided.

Propagate mach_params.dmic_num to ctx->dmic_be_num.
Machine data then controls DMIC BE exposure in this card.

This enables per-audio-client card setup for split configurations.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
Add split_hdmi mode to register separate analog and HDMI audio
clients when iDisp is present.

Keep the analog card on the selected machine configuration with iDisp
masked out, and register HDMI as a dedicated generic HDA card using
sof-hda-generic-idisp.tplg.

Update unregister handling to support split fallback to the legacy
single-client path when split registration is not used.

Signed-off-by: Peter Ujfalusi <peter.ujfalusi@linux.intel.com>
@ujfalusi

ujfalusi commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Changes since v1:

  • patches moved around
  • ARM compile tested
  • the pcm/topology/control/compress code is now linked to snd-sof-audio and not into the snd-sof (core)
  • the HEAD of the PR is mostly what the end result I expect it to be, but there are rough churn going on
  • series is bisect safe for x64 and aarch64

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant